/*
 * Copyright (c) 2012 Universidad Antonio de Nebrija.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License v2.0
 * which accompanies this distribution, and is available at
 * http://www.apache.org/licenses/LICENSE-2.0.html
 * 
 * Contributors:
 *    Universidad Antonio de Nebrija - initial API, implementation and documentation
 */
package com.nebrija.riif.model;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.Locale;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.nebrija.riif.model.resolver.ConditionalOpResolver;
import com.nebrija.riif.model.resolver.LogicalResolver;


/**
 * Util class for conversion and to resolve references in the model
 * Needs refactoring and optimization
 * @author asanchep
 *
 */
public class Util {

	public static final String PROPERTY_SEPARATOR="'";
	public static final String OPENING_PARENTHESES="(";
	public static final String CLOSING_PARENTHESES=")";
	public static final String COMMA=",";
	public static final String OPEN_MATRIX="[";
	public static final String MATRIX_SEP=":";
	public static final String CLOSE_MATRIX="]";
	
	protected static final String INDEX_SEP="_";
	/** pattern to find function names */
	private static final Pattern PATTERN = Pattern.compile("[_A-Za-z0-9\\.]*\\("); 

	public static final Comparator<String> SORT_BY_SPECIAL_INDEX = new Comparator<String>() {

		@Override
		public int compare(String arg0, String arg1) {
			int index0 = arg0.indexOf(INDEX_SEP);
			int index1 = arg1.indexOf(INDEX_SEP);
			if(index0!=index1){
				return index0-index1;
			}
			String s0 = arg0.substring(0,index0);
			String s1 = arg1.substring(0,index1);
			int comp1 = s0.compareTo(s1);
			if(comp1!=0){
				return comp1;
			}
			s0=arg0.substring(index0+1);
			s1=arg1.substring(index1+1);
			return s0.compareTo(s1);
		}
		
	};
	
	public static Object resolveConstant(String value, Component c){
		Constant co = c.getConstant(value);
		if (co==null){
			return value;
		}
		return co.getProperty(RIIFProperties.VALUE_KEY);
	}

	/**
	 * Function to process properties, resolving references and converting them to the numeric value
	 * if possible
	 * @param value The value of the constant
	 * @param c The component where the constant is contained
	 * @return The calculated value
	 */
	public static Object resolveProperty(String value, RIIFPropertiesContainer c){
		// This function can receive values such as "CPU'MBE", "MBE'rate", "MY_CONSTANT" ...
		
		// By default, we look for the key "value" which does not exist in the RIIF file
		// but we create to store the actual value of a constant or parameter.
		String property = RIIFProperties.VALUE_KEY;
		
		// This will be the element to be located. By default, the whole value as in MY_CONSTANT 
		String elementName = value;
		
		// Looking for ' character to see if the property requested is different
		int sepIndex = value.indexOf(PROPERTY_SEPARATOR);
		
		// Variables to check if the property is a matrix with indices
		boolean matrix = false;
		int initMatrix = 0;
		int endMatrix = 0;
		
		// If the value contains a ' then the property is not the value itself. 
		if(sepIndex!=-1){
			// The property is after the ' character, e.g. MBE in CPU'MBE or rate in MBE'rate
			property = value.substring(sepIndex+1);
			// The actual element where the property belongs to. E.g. CPU in CPU'MBE or MBE in MBE'rate
			elementName = value.substring(0,sepIndex);
		}
		
		// The element is retrieved from the container to get the property.
		RIIFElement element = c.getElement(elementName);

		// It can be a single ChildComponent if the container is a Component
		if (element != null && element instanceof ChildComponentUnit){
			ChildComponentUnit comp = (ChildComponentUnit)element;
			// In this case, we get the property which is a RIIFProperties object
			// and assign it to the element to be processed later
			element = comp.getElement(property);
			// The property will be the "value" of the child component
			property = RIIFProperties.VALUE_KEY;
		}
		// It can be an array of ChildComponents if the container is a component
		if (element instanceof ChildComponentList){
			
			int matrixIndex = property.indexOf(OPEN_MATRIX);
			if(matrixIndex==-1){
				return value;
			}
			// We split the property string into the property itself and the array information:
			// property[...] --> property     [...]
			String matrixData = property.substring(matrixIndex);
			property = property.substring(0,matrixIndex);
			
			// The array information can be [N] or [1:N]
			sepIndex = matrixData.indexOf(MATRIX_SEP);
			if(sepIndex==-1){ // only requests 1 element, get it and treat it as with the ChildComponentUnit
				ChildComponentList list = (ChildComponentList) element;
				int idx=Util.getInteger(matrixData.substring(1,matrixData.length()-2), c);
				if(idx==-1){
					return value;
				}
				ChildComponentUnit cu = list.getChild(idx);
				element = cu.getElement(property);
				property = RIIFProperties.VALUE_KEY;
			}else{
				// Get the array information to be processed later 
				try{
					initMatrix = Util.getInteger(matrixData.substring(1,sepIndex),c);
					endMatrix = Util.getInteger(matrixData.substring(sepIndex+1,matrixData.length()-1),c);
					if(initMatrix==-1 || endMatrix==-1){
						return value;
					}
					matrix=true;
				}catch(Exception e){
					return value;
				}				 
			}
		}
		// The string can correspond to a number so we convert it into a double object.
		if (element == null ){
			try{//check if it is a number itself and not a property
				Double d = Double.parseDouble(value);
				return d;
			}catch(NumberFormatException nfe){}
			try{//check if it is a ValueTimeUnit
				ValueTimeUnit vtu = ValueTimeUnit.stringToVTU(value);
				if (vtu!=null){return vtu;}
			}catch(Exception e){}
			return value;
		}
		// It it is not an array we get the property value and return it
		if (!matrix){
			return resolveSingleProperty(element, property, value, c);
		}
		
		//If it is a ChildComponentList with an array of ChildComponentUnit [X:Y]
		//the method will return an arraylist of objects with the values for each element
		ArrayList<Object> result = new ArrayList<Object>();
		ChildComponentList list = (ChildComponentList) element;
		String property2=property;
		property=RIIFProperties.VALUE_KEY;
		for(int i=initMatrix-1;i<endMatrix;i++){
			ChildComponentUnit ccu = list.getChild(i);
			result.add(resolveSingleProperty(ccu.getElement(property2), property, value, ccu));
		}
		return result;
	}	
	
	private static int getInteger(String data, RIIFPropertiesContainer c){
		// data can be a number or a string corresponding to a constant or other property
		int idx = -1; 
		try{
			// Check if it is a number
			idx = Integer.parseInt(data);
		}catch(NumberFormatException e){
			try{
				// Check if we can find the property value and convert it to a number
				idx=(Integer)resolveProperty(data, c);
			}catch(Exception xe){
			}
		}
		return idx;
	}
	
	/**
	 * Resolve a single property
	 * @param element the RIIFProperties element where the properties are stored 
	 * @param property the property to be resolved
	 * @param value the original string with the value we are resolving
	 * @param c the container component
	 * @return
	 */
	private static Object resolveSingleProperty(RIIFElement element, String property, String value, RIIFPropertiesContainer c){
		RIIFProperties prop = (RIIFProperties) element;
		
		// Check first if the property was already calculated. It would be a property with the
		// same name and a specific suffix.
		Object res = prop.getProperty(property + RIIFProperties.CALCULATED_SUFFIX);
		if (res ==null) {
			// It is not calculated, so get the property value
			res = prop.getProperty(property);
		}
		// Some properties use rate instead of VALUE
		if(res==null && property == RIIFProperties.VALUE_KEY){ 
			property=RIIFProperties.RATE_KEY;
			res = prop.getProperty(property);
		}
		// Not found, return the string to be resolved as is
		if(res==null){
			return value;
		}
		// If it is a string, it means that it needs to be resolved too.
		if(res instanceof String){
			Object res2 = res;
			res2 = resolveFormula((String)res, c);
			// If not found and the container is a ChildComponentUnit, we might need to look for it in the childcomponent's parent 
			if (res.equals(res2) && c instanceof ChildComponentUnit){
				Component c2 = ((ChildComponentUnit)c).getContainer();
				res2 = resolveFormula((String)res, c2);
			}
			if(!(res2 instanceof String) || !res.equals((String) res2)){
				prop.setProperty(property + RIIFProperties.CALCULATED_SUFFIX, res2);
				res=res2;
			}
		}
		// If it is a rate and it has units, we return a ValueUnit object with the value and the units
		if(property==RIIFProperties.RATE_KEY){
			String unit = (String)prop.getProperty(RIIFProperties.UNIT);
			if(unit!=null){
				return new ValueUnit(convertToDouble(res), unit);
			}
		}
		return res;		
	}

	/**
	 * Resolves a formula (e.g. MBE'rate*100/X_A)
	 * @param formula The formula
	 * @param c the component which contains the object with the formula
	 * @return the final value
	 */
	public static Object resolveFormula(String formula, RIIFPropertiesContainer c){
		// First resolve the functions in the string
		Object preProcessed = Util.resolveFunctions(formula, c);
		// If the result is not a String, then we have the final value
		if(!(preProcessed instanceof String)){
			return preProcessed;
		}
		// Then resolve the rest, starting for the internal parentheses
		// If no parentheses are found, continue by checking if it is a comparison/condition
		LogicalResolver logical = new LogicalResolver();
		Object resLogical = logical.resolve((String)preProcessed, c);
		if(!(resLogical instanceof String) || ! preProcessed.equals(resLogical)){
			return resLogical;
		}
		ConditionalOpResolver conditional = new ConditionalOpResolver();
		Object resCond = conditional.resolve((String)preProcessed, c);
		if(!(resCond instanceof String)){
			return resCond;
		}

		Object result = processInternalParentheses((String)resCond, c);//computeMulOperands(formula, c);
		return result;
	}
	
	/**
	 * Parses multiplication operation
	 */
	public static Object computeMulOperands(String formula, RIIFPropertiesContainer c){
		// Find the operands by dividing the string in two parts based on the * operand
		int indexOfMul = formula.indexOf("*");
		// If no more multiplication is found then we continue with division
		if (indexOfMul==-1){
			return computeDivOperands(formula, c);
		}
		// Check for additional multiplications (e.g. a * b * c )
		Object first = computeMulOperands(formula.substring(0,indexOfMul), c);
		Object last = computeMulOperands(formula.substring(indexOfMul+1), c);

		return multiply(first,last);
	}

	/**
	 * Parses division operation
	 */
	public static Object computeDivOperands(String formula, RIIFPropertiesContainer c){
		// Find the operands by dividing the string in two parts based on the / operand
		int indexOfDiv = formula.indexOf("/");
		// If no more division is found then we continue with property 
		if (indexOfDiv==-1){
			return resolveProperty(formula, c);
		}
		// Check for additional divisions (e.g. a/b/c )
		Object first = computeDivOperands(formula.substring(0,indexOfDiv), c);
		Object last = computeDivOperands(formula.substring(indexOfDiv+1), c);

		return divide(first,last);
	}	

	/**
	 * Parses subtraction operation and unary minus
	 * cambioOp due to the weird parse order we follow
	 */
	public static Object computeSubtractOperands(String formula, RIIFPropertiesContainer c, boolean cambioOp){
		// Find the operands by dividing the string in two parts based on the - operand
		int indexOfMinus = formula.indexOf("-");
		// If no more subtraction is found then we continue with addition
		if (indexOfMinus==-1){
			return computeAddOperands(formula, c, cambioOp);
		}
		// Check for additional subtractions (e.g. a-b-c )
		Object first = computeSubtractOperands(formula.substring(0,indexOfMinus), c, cambioOp);
		Object last = computeSubtractOperands(formula.substring(indexOfMinus+1), c, true);

		return (cambioOp && indexOfMinus!=0)?add(first,last):subtract(first,last);
	}	


	/**
	 * Parses modulo operation (remainder calculation)
	 */	
	public static Object computeRemainderOperands(String formula, RIIFPropertiesContainer c){
		int indexOfMod = formula.indexOf("%");
		// If no more addition is found then we continue with multiplication
		if (indexOfMod==-1){
			return computeMulOperands(formula, c);
		}
		Object first = computeRemainderOperands(formula.substring(0,indexOfMod), c);
		Object last = computeRemainderOperands(formula.substring(indexOfMod+1), c);

		return remainder(first,last);
	}	
	
	/**
	 * Parses addition operation
 	 * cambioOp due to the weird parse order we follow
	 */	
	public static Object computeAddOperands(String formula, RIIFPropertiesContainer c, boolean cambioOp){
		int indexOfPlus = formula.indexOf("+");
		// If no more addition is found then we continue with multiplication
		if (indexOfPlus==-1){
			return computeRemainderOperands(formula, c);
		}
		Object first = computeAddOperands(formula.substring(0,indexOfPlus), c, false);
		Object last = computeAddOperands(formula.substring(indexOfPlus+1), c, false);

		return (cambioOp)?subtract(first,last):add(first,last);
	}


	/**
	 * Find the remainder between two objects converting them into values first if possible
	 */
	public static Object remainder (Object first, Object last){
		if (first instanceof String  
				|| last instanceof String){
			return first + "%" + last;
		}
		return convertToDouble(first)%convertToDouble(last);
	}

	/**
	 * Subtracts two objects converting them into values first if possible
	 */
	public static Object subtract (Object first, Object last){
		if ((first instanceof String && !first.equals(""))  
				|| last instanceof String){
			return first + "-" + last;
		}
		if (first.equals("")){
			return -convertToDouble(last);
		}
		return convertToDouble(first)-convertToDouble(last);
	}	
	
	/**
	 * Adds two objects converting them into values first if possible
	 */
	public static Object add (Object first, Object last){
		if (first instanceof String   
				|| last instanceof String){
			return first + "+" + last;
		}
		return convertToDouble(first)+convertToDouble(last);
	}
	
	/**
	 * Multiply two objects converting them into values first if possible
	 */
	public static Object multiply (Object first, Object last){
		if (first instanceof String || last instanceof String){
			return first + "*" + last;
		}
		return convertToDouble(first)*convertToDouble(last);
	}

	/**
	 * Divide two objects converting them into values first if possible
	 */
	public static Object divide (Object first, Object last){
		if (first instanceof String || last instanceof String){
			return first + "/" + last;
		}
		return convertToDouble(first)/convertToDouble(last);
	}	
	
	/**
	 * Process grouping parentheses 
	 * @param formula
	 * @param c
	 * @return
	 */
	public static Object processInternalParentheses(String formula, RIIFPropertiesContainer c){
		int indexOfParentheses = formula.indexOf(Util.OPENING_PARENTHESES);

		if(indexOfParentheses==-1){

			//Otherwise, processing subtraction
			return computeSubtractOperands(formula, c,false);
		}
		// Look for matching closing parentheses
		boolean closed =false;
		int indexOfClosing = formula.indexOf(Util.CLOSING_PARENTHESES);
		int internalOpening = indexOfParentheses;
		do{
			internalOpening = formula.indexOf(Util.OPENING_PARENTHESES, internalOpening+1);
			if(internalOpening!=-1 && internalOpening<indexOfClosing){
				indexOfClosing = formula.indexOf(Util.CLOSING_PARENTHESES, indexOfClosing+1);
			}else{
				closed=true;
			}
		}while (!closed);
		Object first = null;
		Object third = null;
		// Recurring call for the string before, inside and after the parentheses
		if (indexOfParentheses>0){
			first = processInternalParentheses(formula.substring(0, indexOfParentheses), c);
		}
		Object second=processInternalParentheses(formula.substring(indexOfParentheses+1,indexOfClosing), c);
		if (indexOfClosing<formula.length()-1){
			third = processInternalParentheses(formula.substring(indexOfClosing+1), c);
		}
		//convert to string that will be processed later
		StringBuilder sb = new StringBuilder();
		if(first!=null){
			sb.append(first);
		}
		if(second instanceof String){
			sb.append(OPENING_PARENTHESES).append(second).append(CLOSING_PARENTHESES);
		}else{
			sb.append(second);
		}
		if(third!=null){
			sb.append(third);
		}
		return sb.toString();
	}

	/**
	 * Resolve function parameters and returns the result in an ArrayList
	 * @param formula the string to be resolved
	 * @param c the component
	 * @return a list of parameters with their dependencies resolved.
	 */
	public static ArrayList<Object> splitCommas(String formula, RIIFPropertiesContainer c){
		int indexOfComma = formula.indexOf(Util.COMMA);

		if(indexOfComma==-1){
			ArrayList<Object> al = new ArrayList<Object>();
			// If no more commas are found, we resolve the parameter and add it to the arraylist
			LogicalResolver logical = new LogicalResolver();
			Object resLogical = logical.resolve(formula, c);
			if(!(resLogical instanceof String) || ! formula.equals(resLogical)){
				al.add(resLogical);
				return al;
			}
			ConditionalOpResolver conditional = new ConditionalOpResolver();
			Object resCond = conditional.resolve(formula, c);
			if(!(resCond instanceof String)){
				al.add(resCond);
				return al;
			}
			formula=(String)resCond;
			al.add(computeSubtractOperands(formula, c, false));
			return al;
		}
		//Recursive call for each part
		ArrayList<Object> first = splitCommas(formula.substring(0,indexOfComma), c);
		ArrayList<Object> last = splitCommas(formula.substring(indexOfComma+1), c);

		// Add both parts into a common arraylist
		first.addAll(last);
		return first;
	}	
	
	public static Double convertToDouble(Object element){
		if (element instanceof Integer){
			return (Double)(0.0d + (Integer)element);
		} 
		if (element instanceof Long){
			return (Double)(0.0d + (Long)element);
		} 
		if (element instanceof Float){
			return (Double)(0.0d + (Float)element);
		} 
		if (element instanceof Double){
			return (Double)element;
		} 
		if (element instanceof String){
			return Double.parseDouble((String)element);
		} 
		if (element instanceof ValueUnit){
			return ((ValueUnit)element).getValue();
		}
		return new Double(0);
	}
	
	/**
	 * Method to resolve functions
	 * @param formula the string with the information to be resolved
	 * @param c the component containing the formula
	 * @return the resulting object
	 */
	public static Object resolveFunctions(String formula, RIIFPropertiesContainer c){
		
		// We store the functions located in the string in a sortedmap indexed by the size of the text inside the parentheses followed by _ and the position
		SortedMap<String, FunctionLocation> diffFunctions = new TreeMap<String, FunctionLocation>(SORT_BY_SPECIAL_INDEX);
		
		Matcher m = PATTERN.matcher(formula);
		// Search for the function pattern and create an object of the FunctionLocation auxiliary class to
		// store the name of the function, its location and name length
		while(m.find()){
			FunctionLocation lf = new FunctionLocation(formula.substring(m.start(0),m.end(0)-1), m.start(0));
			lf.setEnd(Util.findClosingParentheses(lf.getInit()+lf.getName().length(), formula));
			diffFunctions.put(lf.getEnd()-lf.getInit()-lf.getName().length()+INDEX_SEP+lf.getInit(), lf); 
		}
		
		Collection<FunctionLocation> values = diffFunctions.values();
		
		// Traverse the sorted collection starting by the ones with smaller size of the string inside the parentheses
		// So we ensure that inner functions are processed first
		for(FunctionLocation l: values){
			// Process the parameters
			ArrayList<Object> params = splitCommas(formula.substring(l.getInit()+l.getName().length()+1,l.getEnd()),c);
			// Retrieve the object of the class that implements the function
			FunctionInterface fi = l.getFunction();
			// If it does not exist, then rewrite the string with the resolved parameters instead of the original information
			if(fi==null){
				StringBuilder sb = new StringBuilder(l.getName()).append(OPENING_PARENTHESES);
				boolean comma=false;
				for (Object o:params){
				    if(comma){
				    	sb.append(COMMA);
				    }
					comma=true;
			    	sb.append(o.toString());
				}
				sb.append(CLOSING_PARENTHESES);
				// Find the string that replace the original one
				String replaced = formula.substring(0,l.getInit())+sb.toString()+formula.substring(l.getEnd()+1);
				//Update the indices of diffFunctions as the start and end positions have changed
				Util.updateFunctionLocationPositions(diffFunctions, formula.length()-replaced.length(), l.getInit(), l.getEnd());
				formula=replaced;
				continue;
			}
			
			// Process the parameters executing the implementation of the function
			Object functionVal = fi.processOperands(params.toArray());
			// Replace the original string by one with the resulting value in the appropriate position
			StringBuilder resString = new StringBuilder();
			if(l.getInit()>0){
				resString.append(formula.substring(0,l.getInit()));
			}
			
			resString.append((functionVal instanceof Double)?String.format(Locale.US, "%f", functionVal):functionVal);
			
			if(l.getEnd()<formula.length()-1){
				resString.append(formula.substring(l.getEnd()+1));
			}
			//Update the indices of diffFunctions as the start and end positions have changed
			Util.updateFunctionLocationPositions(diffFunctions, formula.length()-resString.length(), l.getInit(), l.getEnd());
			formula=resString.toString();
		}
		
		return formula;
		
	}
	
	/**
	 * Update the indices of diffFunctions as the start and end positions have changed
	 * @param diffFunctions Registry of function names found in the string to be resolved
	 * @param diff size diference between the original string and the updated one
	 * @param init init of the parentheses
	 * @param end end of the parentheses
	 */
	protected static void updateFunctionLocationPositions (SortedMap<String, FunctionLocation> diffFunctions, int diff, int init, int end){
		Collection<FunctionLocation> flocs = diffFunctions.values();
		for (FunctionLocation func: flocs){
			int finit = func.getInit();
			int fend = func.getEnd();
			if(finit==init && fend==end){
				continue; // The function that has been just applied
			}
			if(fend<init){ // not affected
				continue;
			}
			// Change function end
			func.setEnd(fend-diff);
			
			if (finit>end){
				func.setInit(finit-diff);
			}
		}
	}
	
	/**
	 * Looks for a closing parenthesis corresponding an initial one
	 * @param par index of the opening parentheses in the formula
	 * @param formula string to be resolved
	 * @return
	 */
	protected static int findClosingParentheses(int par, String formula){
		int internalOpening = par;
		boolean closed=false;
		int indexOfClosing = formula.indexOf(Util.CLOSING_PARENTHESES, par+1);
		do{
			internalOpening = formula.indexOf(Util.OPENING_PARENTHESES, internalOpening+1);
			// When additional opening parentheses are found, look for the appropriate closing parentheses
			if(internalOpening!=-1 && internalOpening<indexOfClosing){
				indexOfClosing = formula.indexOf(Util.CLOSING_PARENTHESES, indexOfClosing+1);
			}else{
				closed=true;
			}
		}while (!closed);
		return indexOfClosing;
	}
	
}
